using System.Reflection;

namespace TheSalLab.MauiBehaviors;

public class MauiEventHandlerBehavior : MauiBehaviorPropertiesBase {
    private Delegate _eventHandler;

    private object _resolvedSource;

    public static readonly BindableProperty EventNameProperty =
        BindableProperty.Create(nameof(EventName), typeof(string),
            typeof(MauiEventHandlerBehavior),
            propertyChanged: OnEventNameChanged);

    public static readonly BindableProperty SourceObjectProperty =
        BindableProperty.Create(nameof(SourceObject), typeof(object),
            typeof(MauiEventHandlerBehavior),
            propertyChanged: OnSourceObjectChanged);

    public string EventName {
        get => (string)GetValue(EventNameProperty);
        set => SetValue(EventNameProperty, value);
    }

    public object SourceObject {
        get => GetValue(SourceObjectProperty);
        set => SetValue(SourceObjectProperty, value);
    }

    protected override void OnAttachedTo(VisualElement bindable) {
        base.OnAttachedTo(bindable);
        SetResolvedSource(ComputeResolvedSource());
    }

    protected override void OnDetachingFrom(VisualElement bindable) {
        base.OnDetachingFrom(bindable);
        SetResolvedSource(null);
    }

    private void SetResolvedSource(object newSource) {
        if (AssociatedObject is null || _resolvedSource == newSource) {
            return;
        }

        if (_resolvedSource is not null) {
            DeregisterEvent(EventName);
        }

        _resolvedSource = newSource;

        if (_resolvedSource is not null) {
            RegisterEvent(EventName);
        }
    }

    private object ComputeResolvedSource() => SourceObject ?? AssociatedObject;

    private void RegisterEvent(string eventName) {
        if (string.IsNullOrWhiteSpace(eventName)) {
            return;
        }

        var sourceObjectType = _resolvedSource.GetType();
        var eventInfo = sourceObjectType.GetRuntimeEvent(eventName);
        if (eventInfo is null) {
            return;
        }

        var methodInfo = typeof(MauiEventHandlerBehavior).GetTypeInfo()
            .GetDeclaredMethod("OnEvent");
        _eventHandler = methodInfo.CreateDelegate(eventInfo.EventHandlerType,
            this);
        eventInfo.AddEventHandler(_resolvedSource, _eventHandler);
    }

    private void DeregisterEvent(string eventName) {
        if (string.IsNullOrWhiteSpace(eventName)) {
            return;
        }

        if (_eventHandler is null) {
            return;
        }

        var eventInfo = _resolvedSource.GetType().GetRuntimeEvent(eventName);
        if (eventInfo is null) {
            throw new ArgumentException(
                $"{nameof(MauiEventHandlerBehavior)}: Can't de-register the '{EventName}' event.");
        }

        eventInfo.RemoveEventHandler(_resolvedSource, _eventHandler);
        _eventHandler = null;
    }

    private async void OnEvent(object sender, object eventArgs) {
        foreach (var bindable in Actions) {
            bindable.BindingContext = BindingContext;
            var action = (IMauiAction)bindable;
            await action.Execute(sender, eventArgs);
        }
    }

    private static void OnEventNameChanged(BindableObject bindable,
        object oldValue, object newValue) {
        var behavior = (MauiEventHandlerBehavior)bindable;
        if (behavior.AssociatedObject is null ||
            behavior._resolvedSource is null) {
            return;
        }

        var oldEventName = (string)oldValue;
        var newEventName = (string)newValue;

        behavior.DeregisterEvent(oldEventName);
        behavior.RegisterEvent(newEventName);
    }

    private static void OnSourceObjectChanged(BindableObject bindable,
        object oldValue, object newValue) {
        var behavior = (MauiEventHandlerBehavior)bindable;
        behavior.SetResolvedSource(behavior.ComputeResolvedSource());
    }
}